package com.tjc.cloud.util.jwt.client.config


import com.nimbusds.jose.crypto.DirectDecrypter
import com.nimbusds.jwt.EncryptedJWT
import com.tjc.cloud.model.auth.Account
import org.apache.commons.codec.binary.Base64
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.beans.factory.getBeansWithAnnotation
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.stereotype.Controller
import org.springframework.util.AntPathMatcher
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.X509EncodedKeySpec
import javax.crypto.spec.SecretKeySpec
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.memberProperties


@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class AutoConfiguration {
    @Value("\${jwt.encryption}")
    private lateinit var encryption: String
    @Value("\${jwt.publicKey}")
    private lateinit var publicKey: String

    @Autowired
    private lateinit var security: Security


    class Security(var whiteList: List<String> = listOf())

    @Autowired
    lateinit var applicationContext: ApplicationContext

    @Bean
    @ConfigurationProperties("spring.security")
    fun security(): Security = Security()


    data class Router(var path: String = "", var authority: String, var method: String? = null)


    fun ApplicationContext.controllers(): Flux<KClass<out Any>> = Mono.justOrEmpty(this).flatMapIterable {
        it.getBeansWithAnnotation<Controller>().values
    }.map { it::class }

    fun KClass<out Any>.basePath(): Flux<String> = Mono.justOrEmpty<RequestMapping>(this.findAnnotation<RequestMapping>())
            .flatMapMany {
                Flux.fromArray( it.path.plus(it.value))
            }.switchIfEmpty(Flux.just(""))


    fun KClass<out Any>.paths() = this.basePath().flatMap { basePath ->
        this.memberFunctions.toFlux().filter { it.findAnnotation<RolesAllowed>() != null }.flatMap { router ->

            val allMethods = Flux.just(GetMapping::class, PostMapping::class, PutMapping::class, PatchMapping::class, DeleteMapping::class)

            Mono.justOrEmpty<RolesAllowed>(router.findAnnotation<RolesAllowed>()).flatMapMany { it.value.toFlux() }
                    .flatMap { authority ->
                        Mono.justOrEmpty(router.annotations).flatMapIterable { it }.filterWhen {
                            allMethods.filter { method ->
                                it::annotationClass.get() == method
                            }.map { true }
                                    .defaultIfEmpty(false)
                        }.flatMap { method ->
                            method.annotationClass.memberProperties.toFlux().filter {
                                listOf("path", "value").contains(it.name)
                            }.flatMap {
                                (it.call(method) as Array<String>).toFlux()
                            }.map { path ->
                                Router("/$basePath/$path".replace(Regex("/+"), "/"), authority, method.annotationClass.findAnnotation<RequestMapping>()?.method?.first()?.name)
                            }.defaultIfEmpty(
                                    Router(basePath, authority, method.annotationClass.findAnnotation<RequestMapping>()?.method?.first()?.name)
                            )
                        }
                    }

        }
    }

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain =
            http.csrf().disable()
                    .authorizeExchange().toMono().flatMap { chain ->
                        applicationContext.controllers()
                                .flatMap { controller ->
                                    controller.paths()
                                }.reduce(chain) { a, s ->
                                    println(s)
                                    a.pathMatchers(HttpMethod.valueOf(s.method!!), s.path)
                                            .access { authentication, context ->
                                                val authorities = authentication.flatMapIterable { it.authorities }.map { it.authority }
                                                authorities.hasElement("ROLE_MANAGER")
                                                        .filter { it }
                                                        .switchIfEmpty(
                                                                authorities.filterWhen { authority ->
                                                                    authentication.map { it.principal }
                                                                            .cast(Jwt::class.java)
                                                                            .map {
                                                                                mapOf("@id" to it.id, "@cid" to it.claims["cid"])
                                                                                        .plus(context.variables)
                                                                            }
                                                                            .map {
                                                                                it.plus(AntPathMatcher().extractUriTemplateVariables(s.authority, authority)) == it
                                                                            }
                                                                            .onErrorReturn(false)
                                                                }.hasElements()).map { AuthorizationDecision(it) }
                                            }
                                }
                    }.map { it.and() }
                    .map { security ->
                        security
                                .authorizeExchange()
                                .pathMatchers(* this.security.whiteList.toTypedArray(), "favicon.ico", "/csrf", "/swagger-ui.html",  "/webjars/**", "/v3/**")
                                .permitAll()
                                .and()
                                .httpBasic().disable()
                                .formLogin().disable()
                                .authorizeExchange()
                                .anyExchange()
                                .authenticated()
                                .and()
                                .oauth2ResourceServer().jwt().jwtDecoder {
                                    val jwe = EncryptedJWT.parse(it)
                                    val keyBytes = encryption.toByteArray()
                                    val secretKey = SecretKeySpec(keyBytes, 0, keyBytes.size, "AES")
                                    jwe.decrypt(DirectDecrypter(secretKey))
                                    val key = KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(Base64.decodeBase64(publicKey)))
                                    NimbusReactiveJwtDecoder(key as RSAPublicKey).decode(jwe.payload.toString())
                                }
                                .jwtAuthenticationConverter { jwt ->
                                    jwt.claims.toMono().map { map ->
                                        val authorities = (map["\$int_roles"] as? List<String>
                                                ?: listOf()).map {
                                            "ROLE_$it"
                                        }.plus(map["\$int_perms"] as? List<String> ?: listOf())
                                                .plus("CID_${map["cid"]}".toUpperCase())
                                                .map {
                                            SimpleGrantedAuthority(it)
                                        }
                                        println(authorities)
                                        JwtAuthenticationToken(jwt, authorities)
                                    }
                                }
                                .and().and()
                                .build()
                    }.block()!!


    companion object {
        @Bean
        fun passwordEncoder(): PasswordEncoder =
                PasswordEncoderFactories
                        .createDelegatingPasswordEncoder()
    }

}

fun claimsContext(): Mono<Map<String, Any>> = ReactiveSecurityContextHolder.getContext()
        .map {
            it.authentication.principal
        }
        .cast(Jwt::class.java)
        .map { it.claims }

fun accountContext(): Mono<Account> = ReactiveSecurityContextHolder.getContext()
        .map {
            it.authentication.principal
        }
        .cast(Jwt::class.java)
        .map {
            Account(id = it.id,openid = it.claims["openid"] as? String,alipayid = it.claims["alipayid"] as? String, username = it.claims["username"] as? String, cid = it.claims["cid"] as? String)
        }
